Skip to content

fix(runtime)!: the action activation door answers a name two packages hold with 409 naming both, as the execution door does, not 404 "has no declaration" - #33399

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/area-E2b-32689
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/area-E2b-32689

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #32689

Clause-②: yes (narrowing) — POST /api/v1/actions/_activation/OBJECT/NAME for a name two packages hold on the route's key (both object-less with OBJECT = global, or both bound to OBJECT): FROM 404 RESOURCE_NOT_FOUND "has no declaration — there is nothing to switch" TO 409 RESOURCE_CONFLICT naming every holder (error.details.candidates, each with its packageId), for enabled: true and false, nothing written. That is the execution door's answer for the same name. One holder and an undeclared name answer as before.
Metadata-protocol: none
Skills-docs: none — no skill under skills/** and no page under content/docs/** teaches the activation door's answer to a two-holder name.

维护者速读

  • 改了什么:两个包声明同名 action 时(都不挂对象,或挂在同一对象上),激活门 POST /actions/_activation/:object/:action 现在答 409 并列出两个持有包,和执行门一致;之前答 404「has no declaration」。
  • 为什么改:声明解析其实已给出歧义拒绝,激活门却丢掉它,只看「没有 action」就答不存在;把歧义报成不存在违反专题红线「歧义要报告、不许猜」(ADR-0150 D3)。
  • 风险与回滚:收窄类(404 → 409),第一方触达 0 处;两种情况都不写台账,和以前一样。单持有者、完全未声明的名字回答不变。回滚 = revert 本 PR。
  • 先看:packages/runtime/src/domains/actions.ts 第 3 步新增的一行 declaration.refusal 判断;packages/runtime/src/domains/action-activation-unpackaged.test.ts 的两条 NARROWING 与三条 CONTROL;.changeset/32689-activation-door-two-holder-refusal.md。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/action-activation-unpackaged.test.ts,预期 23/23 通过。
  • 需要你做的:无,席位落地(收窄类,落地前需契约复审记录)。

What changes

  • The door. handleActionActivationWrite step 3 (packages/runtime/src/domains/actions.ts): after the degraded branch and before the !declaration.action branch, a reading that carries declaration.refusal is answered with refusedNameAnswer(deps, declaration.refusal) — the shared ADR-0150 D3/D5 helper from catalog-activation.ts, 409 with { code: 'RESOURCE_CONFLICT', candidates }. That is the envelope the execution door answers the same refusal with (admitResolvedAction in action-dispatch.ts: 409, standardErrorCodeForHttpStatus(409), the same candidates). The docblock's step 3 names the new answer. action-execution.ts is not edited; resolveRouteActionDeclaration is consumed as it is on main.
  • Pins (packages/runtime/src/domains/action-activation-unpackaged.test.ts, same real MetadataManager over MemoryLoader and the real activation projection and store):
    • NARROWING — two packages' object-less shared_ping, POST /_activation/global/shared_ping with enabled false and true: 409 RESOURCE_CONFLICT, candidates holding com.acme.alpha and com.acme.beta, both named in the body, no ledger row. (Was a 404 pin.)
    • NARROWING — two packages' shared_ping bound to ONE object, both directions: 409 RESOURCE_CONFLICT naming both packages, no row. (Was a 404 pin.)
    • CONTROL — a name nothing declares, both directions: 404 RESOURCE_NOT_FOUND "has no declaration", no row.
    • CONTROL — one package's object-less shared_ping, each direction on a fresh ledger: 200, one row ['shared_ping', 'com.acme.alpha', enabled]. (New.)
    • CONTROL — one package's action on one object: 200 and its row (existing, unchanged).
  • Changeset. .changeset/32689-activation-door-two-holder-refusal.md: @objectstack/runtime patch, BREAKING FROM → TO, ADR-0087 disposition not-required (no-migration-prescription).

Tests

All at 2bf90b6062, closure built first (pnpm --filter '@objectstack/runtime^...' build, exit 0), under the shared verify lock.

Command Result
pnpm --filter @objectstack/runtime test exit 0 · Test Files 598 passed (598) · Tests 10656 passed, 19 skipped (10675)
pnpm --filter @objectstack/runtime typecheck exit 0 (test layer: 27 files / 183 errors / 66 signatures held in test-typecheck-debt.json, unchanged)
pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/action-activation-unpackaged.test.ts 23/23
pnpm exec eslint --no-inline-config on the two changed source files exit 0

Acceptance notes

  • Red line read (epic design(metadata): address behavioural metadata by package — key flows, actions, jobs and other non-object types by (package, name); re-open ADR-0028 (replaces #30806 phase 3 renames) #31527). Resolution is not loosened: the door answers the reading's own refusal and adds no fallback. The ambiguity is reported with every holder, never guessed. One-holder behaviour is unchanged (two CONTROL pins). Bare-name URLs keep their meaning: the path shape and the undeclared 404 are unchanged.
  • Order of branches. degraded (503) is still read first, as before; the refusal is read before "no declaration". Step 4's same-name-on-two-objects refusal (409 naming the objects) is unchanged; the dogfood pin for it (action-activation-door-live.dogfood.test.ts, one package, two objects) never reaches the new branch.
  • No caller fix exists per package at this door. The ledger addresses an action by name, so neither package's action can be switched here while both hold the name; before this change the same request answered 404 and switched nothing either. The changeset says so.
  • Dependents. CI's Test Core selection for a @objectstack/runtime change is ~50 packages (ui, cli, examples, dogfood). Locally: runtime's own suite and the full dogfood battery (owed by the narrowing arm, which boots the composed stack). The rest is declared to CI: no exported symbol or type changes, only this door's HTTP answer for a two-holder name.

Verification

Ablation of the refusal branch (one ablation, both NARROWING pins)
  • Mutation through scripts/ablation-replace.mjs --delete on packages/runtime/src/domains/actions.ts, anchor if (declaration.refusal) return refusedNameAnswer(deps, declaration.refusal);. On disk: anchor 1 → 0, blob 956da40f97d1 → 962849a03558. The test resolves the door through the relative ../http-dispatcher.js import, so the source is what runs; no build leg.
  • Red: Tests 2 failed | 21 passed (23). The two NARROWING pins failed with the BASE answer, {"code":"RESOURCE_NOT_FOUND","message":"Action 'shared_ping' on 'global' has no declaration — there is nothing to switch off. …","httpStatus":404} (and the same on crm_contact). Every CONTROL stayed green.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH; blob after restore 956da40f97d1 = HEAD blob, git diff HEAD empty, anchor count 1. Restored run: 23/23.
  • main has not changed actions.ts since this branch's base d4fbb4dbb, so the ablated door is main's door: the pins are red on main.
Gates (derived at 2bf90b6, 66 commands)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 66 commands, each run with stdin from /dev/null, exit code captured before any pipe.
  • 65 × exit 0. Four dist-reading gates (check:dts-closure, check:lean-entry-closure, check:sourcemap-no-sources-content, check:type-check-debt) answered PREREQUISITE NOT MET before the closure build and exit 0 after it.
  • NOT MEASURED: check:dual-build-cjs-loads, reason: it reads every workspace package's dist/ (packages/adapters/hono, packages/apps/*, …), which a runtime closure build does not produce. CI's Lint & Repo Gates is the reading.
  • --ran reconciliation: 66 derived, 65 run, 1 NOT-MEASURED, 0 UNRUN.
Dogfood battery (owed by the narrowing arm)

Generated by Claude Code

…o-holder refusal 409, not 404

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
…es its row in both directions (CONTROL)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
…TO 409 (narrowing)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e0ce1b523c9cdb625211fc866593f57aee9223ee → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9fba295ec7dfa248e7bdb603a6c18eae5ea455c4 — the merge of head 2bf90b60626483f25bb4d68c9c35f77e4eb064d1 into base e0ce1b523c9cdb625211fc866593f57aee9223ee, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9fba295ec7dfa248e7bdb603a6c18eae5ea455c4 && git checkout 9fba295ec7dfa248e7bdb603a6c18eae5ea455c4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e0ce1b523c9cdb625211fc866593f57aee9223ee 2bf90b60626483f25bb4d68c9c35f77e4eb064d1 && git checkout -B drift-repro e0ce1b523c9cdb625211fc866593f57aee9223ee && git merge --no-ff 2bf90b60626483f25bb4d68c9c35f77e4eb064d1

node scripts/docs-audit/affected-docs.mjs --json e0ce1b523c9cdb625211fc866593f57aee9223ee

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyWLSMyXZGc6dwhCAu
Request: review · area E2b · head 2bf90b6 · 2026-11-14T19:21Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 2bf90b6 · 2026-11-14T19:21:54Z

review#3.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2bf90b60626483f25bb4d68c9c35f77e4eb064d1
Local-runs: none
Scope: packages/runtime/src/domains/actions.ts, .changeset/32689-activation-door-two-holder-refusal.md + packages/runtime/src/domains/catalog-activation.ts (refusedNameAnswer), packages/runtime/src/domains/action-activation-unpackaged.test.ts

① Derived judgments

  • Placement. if (declaration.refusal) return refusedNameAnswer(deps, declaration.refusal) sits after the degraded → 503 branch (an outage still reads as an outage) and before the !declaration.action → 404 branch, which used to swallow the refusal as absence. The body has already been read and validated (step 2), so a malformed body still answers 400 first. Nothing is written on the new arm.
  • Envelope. refusedNameAnswer is the execution door's own answer (409, code: RESOURCE_CONFLICT, candidates from the reading). The two doors now agree on the same name, as the docblock's "can never disagree" promise requires. The candidates are package ids already shown by the execution door to the same audience, and the activation door's own authority gate (step 1) runs before this.
  • Pins. Both object-less and both bound to one object: 409 with both candidates, in both directions, with no ledger row. CONTROLs: a name nothing declares stays 404 "has no declaration"; one package's action flips and writes its row; one package's action bound to two objects keeps the door's own 409.
  • Declaration. Clause-②: yes (narrowing) (a 404 refusal becomes a 409 refusal for the same request, so an existing caller's observed outcome moves) with fix(runtime)!, a BREAKING FROM→TO, the caller's fix, and one adr-0087: not-required (no-migration-prescription) marker. Metadata-protocol: none. patch (AGENTS.md: non-protocol ships patch).

② Semver level

@objectstack/runtime: patch, BREAKING banner. Correct.

③ Boundary flags

④ Design judgment

ADR-0150 D3 says ambiguity is reported, never answered as absence. A 404 "has no declaration" for a name two packages declare tells the operator the action does not exist when the truth is that the door cannot choose. Reusing the execution door's refusal keeps one answer per name across both doors. Leaving the ledger untouched in both directions is right, because the ledger addresses by name and cannot carry a package choice. The design holds.

Implemented-by: claude/area-E2b-32689
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyWLSMyXZGc6dwhCAu
ACCEPT · area E2b (area-lease pilot #33059) · 2026-11-14T19:25Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 19:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit d3ceb1d Nov 14, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/area-E2b-32689 branch November 14, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants